/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.jfonia.xml;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.jfonia.constants.ModelConstants;
import org.jfonia.view.main.LeadSheet;
import org.jfonia.model.Sequence;
import org.jfonia.model.Staff;
import org.jfonia.model.StaffCollection;
import org.jfonia.model.ToneSequence;
import org.jfonia.model.elements.Beat;
import org.jfonia.model.elements.Chord;
import org.jfonia.model.elements.Clef;
import org.jfonia.model.elements.KeySignature;
import org.jfonia.model.elements.Lyric;
import org.jfonia.model.elements.MusicalElement;
import org.jfonia.model.elements.TimeSignature;
import org.jfonia.model.elements.Tone;
import org.jfonia.view.scribbles.Scribble;
import org.jfonia.view.labels.Label;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

/**
 *
 * @author Rik Bauwens
 */
public class Parser
{
        private static Parser instance;

        private final String LEADSHEET = "leadsheet";
        private final String VERSION = "version";
        private final String NAME = "name";
        private final String AUTHOR = "author";
        private final String DESCRIPTION = "description";
        private final String BPM = "bpm";
        private final String INSTRUMENT = "instrument";
        private final String STAFFCOLLECTION = "staffcollection";
        private final String STAFF = "staff";
        private final String LENGTH = "length";
        private final String SEQUENCE = "sequence";
        private final String TYPE = "type";
        private final String NODE = "node";
        private final String DURATION = "duration";
        private final String MEASURE = "measure";
        private final String CHORD = "chord";
        private final String STAFFRANK = "staffrank";
        private final String CENTRALCRANK = "centralcrank";
        private final String LYRIC = "lyric";
        private final String NOMINATOR = "nominator";
        private final String DENOMINATOR = "denominator";
        private final String BASE40RANK = "base40rank";
        private final String SCRIBBLES = "scribbles";
        private final String SCRIBBLE = "scribble";
        private final String X = "x";
        private final String Y = "y";
        private final String TEXT = "text";
        private final String LABELS = "labels";
        private final String LABEL = "label";
        private final String STAFFCOLLECTIONID = "staffcollectionid";
        private final String TICK = "tick";
        private final String PRE = "pre";

        private Parser()
        {

        }

        public static synchronized Parser getInstance()
        {
                if(instance == null)
                        instance = new Parser();
                return instance;
        }

        public LeadSheet readLeadSheet(String fileName)
        {
                try
                {
                        //Create a leadsheet
                        LeadSheet leadSheet = new LeadSheet();

                        //Create a document
                        File file = new File(fileName);
                        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
                        DocumentBuilder builder = builderFactory.newDocumentBuilder();
                        Document document = builder.parse(file);
                        document.getDocumentElement().normalize();

                        Element leadSheetElement = (Element) document.getElementsByTagName(LEADSHEET).item(0);
                        int version = Integer.parseInt(leadSheetElement.getAttribute(VERSION));
                        if(version != ModelConstants.VERSION)
                                return null;
                        leadSheet.setName(leadSheetElement.getAttribute(NAME));
                        leadSheet.setAuthor(leadSheetElement.getAttribute(AUTHOR));
                        leadSheet.setDescription(leadSheetElement.getAttribute(DESCRIPTION));
                        Element bpmElement = (Element) leadSheetElement.getElementsByTagName(BPM).item(0);
                        int bpm = Integer.parseInt(bpmElement.getChildNodes().item(0).getNodeValue());
                        leadSheet.setBPM(bpm);
                        Element instrumentElement = (Element) leadSheetElement.getElementsByTagName(INSTRUMENT).item(0);
                        if(instrumentElement != null && instrumentElement.getChildNodes().getLength() > 0)
                        {
                                String instrument = instrumentElement.getChildNodes().item(0).getNodeValue();
                                leadSheet.setInstrument(instrument);
                        }

                        NodeList staffCollectionNodes = leadSheetElement.getElementsByTagName(STAFFCOLLECTION);
                        for(int i = 0; i < staffCollectionNodes.getLength(); i++)
                        {
                                Element staffCollectionElement = (Element) staffCollectionNodes.item(i);
                                NodeList staffNodes = staffCollectionElement.getElementsByTagName(STAFF);
                                StaffCollection staffCollection = new StaffCollection();
                                leadSheet.addStaffCollection(staffCollection);
                                for(int j = 0; j < staffNodes.getLength(); j++)
                                {
                                        Element staffElement = (Element) staffNodes.item(j);
                                        NodeList sequenceNodes = staffElement.getElementsByTagName(SEQUENCE);
                                        Staff staff = new Staff();
                                        staff.setLength(Integer.parseInt(staffElement.getAttribute(LENGTH)));
                                        staffCollection.addStaff(staff);
                                        for(int k = 0; k < sequenceNodes.getLength(); k++)
                                                staff.addSequence(createSequence(
                                                        (Element) sequenceNodes.item(k)));
                                }
                        }

                        //Scribbles
                        Element scribblesElement = (Element) leadSheetElement.getElementsByTagName(SCRIBBLES).item(0);
                        NodeList scribbleNodes = scribblesElement.getElementsByTagName(SCRIBBLE);
                        for(int i = 0; i < scribbleNodes.getLength(); i++)
                        {
                                Element scribbleElement = (Element) scribbleNodes.item(i);
                                int x = Integer.parseInt(scribbleElement.getAttribute(X));
                                int y = Integer.parseInt(scribbleElement.getAttribute(Y));
                                String text = scribbleElement.getAttribute(TEXT);
                                leadSheet.addScribble(new Scribble(x, y, text));
                        }

                        //Labels
                        Element labelsElement = (Element) leadSheetElement.getElementsByTagName(LABELS).item(0);
                        NodeList labelNodes = labelsElement.getElementsByTagName(LABEL);
                        for(int i = 0; i < labelNodes.getLength(); i++)
                        {
                                Element labelElement = (Element) labelNodes.item(i);
                                int staffCollectionId = Integer.parseInt(labelElement.getAttribute(STAFFCOLLECTIONID));
                                int tick = Integer.parseInt(labelElement.getAttribute(TICK));
                                boolean pre = Boolean.parseBoolean(labelElement.getAttribute(PRE));
                                leadSheet.addLabel(new Label(staffCollectionId, tick, pre));
                        }
                        return leadSheet;
                }
                catch (ParserConfigurationException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch (SAXException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch (IOException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                }
                return null;
        }

        public void writeLeadSheet(String fileName, LeadSheet leadSheet)
        {
                try
                {
                        //Create a document
                        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
                        DocumentBuilder builder = builderFactory.newDocumentBuilder();
                        Document document = builder.newDocument();

                        //Create the XML tree
                        //Create the root element and add it to the document
                        Element leadSheetElement = document.createElement(LEADSHEET);
                        leadSheetElement.setAttribute(VERSION, Integer.toString(leadSheet.getVersion()));
                        leadSheetElement.setAttribute(NAME, leadSheet.getName());
                        leadSheetElement.setAttribute(AUTHOR, leadSheet.getAuthor());
                        leadSheetElement.setAttribute(DESCRIPTION, leadSheet.getDescription());
                        document.appendChild(leadSheetElement);

                        //Beats per minute
                        Element bpmElement = document.createElement(BPM);
                        leadSheetElement.appendChild(bpmElement);
                        Text bpm = document.createTextNode(Integer.toString(leadSheet.getBPM()));
                        bpmElement.appendChild(bpm);

                        //Instrument
                        Element instrumentElement = document.createElement(INSTRUMENT);
                        leadSheetElement.appendChild(instrumentElement);
                        Text instrument = document.createTextNode(leadSheet.getInstrument());
                        instrumentElement.appendChild(instrument);

                        //Staffcollections
                        for (StaffCollection staffCollection : leadSheet) {
                                Element staffCollectionElement = document.createElement(STAFFCOLLECTION);
                                leadSheetElement.appendChild(staffCollectionElement);
                                //Staffs
                                for(Staff staff : staffCollection)
                                {
                                        Element staffElement = document.createElement(STAFF);
                                        staffElement.setAttribute(LENGTH, Integer.toString(staff.getLength()));
                                        staffCollectionElement.appendChild(staffElement);
                                        for(Sequence sequence : staff.getSequences())
                                            staffElement.appendChild(
                                                    createSequenceElement(document, sequence));
                                }
                        }

                        //Scribbles
                        Element scribblesElement = document.createElement(SCRIBBLES);
                        leadSheetElement.appendChild(scribblesElement);
                        for(Scribble scribble : leadSheet.getScribbles())
                        {
                                Element scribbleElement = document.createElement(SCRIBBLE);
                                scribbleElement.setAttribute(X, Integer.toString(scribble.getX()));
                                scribbleElement.setAttribute(Y, Integer.toString(scribble.getY()));
                                scribbleElement.setAttribute(TEXT, scribble.getText());
                                scribblesElement.appendChild(scribbleElement);
                        }
                        //Labels
                        Element labelsElement = document.createElement(LABELS);
                        leadSheetElement.appendChild(labelsElement);
                        for(Label label : leadSheet.getLabels())
                        {
                                Element labelElement = document.createElement(LABEL);
                                labelElement.setAttribute(STAFFCOLLECTIONID, Integer.toString(label.getStaffCollectionId()));
                                labelElement.setAttribute(TICK, Integer.toString(label.getTick()));
                                labelElement.setAttribute(PRE, Boolean.toString(label.isPre()));
                                labelsElement.appendChild(labelElement);
                        }

                        //Output the XML
                        //Set up Transformer
                        TransformerFactory transformerFactory = TransformerFactory.newInstance();
                        Transformer transformer = transformerFactory.newTransformer();
                        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");


                        DOMSource source = new DOMSource(document);
                        
                        // Prepare the output file
                        File file = new File(fileName);
                        Result result = new StreamResult(file);

                        // Write the DOM document to the file
                        transformer.transform(source, result);

                        //Debug
                        //Create string from xml tree
                        //StringWriter writer = new StringWriter();
                        //StreamResult streamResult = new StreamResult(writer);
                        //transformer.transform(source, streamResult);
                        //String xmlString = writer.toString();
                        //Print xml
                        //System.out.println("Here's the xml:\n\n" + xmlString);
                }
                catch (ParserConfigurationException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                } catch (TransformerConfigurationException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                } catch (TransformerException ex)
                {
                        Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
                }

        }

        private Element createSequenceElement(Document document, Sequence sequence)
        {
                Element sequenceElement = document.createElement(SEQUENCE);
                sequenceElement.setAttribute(TYPE, sequence.getTemplateClass().getSimpleName());
                for(int i = 0; i < sequence.size(); i ++)
                {
                        MusicalElement musicalElement = (MusicalElement) sequence.getMusicalElementFromIndex(i);
                        Element nodeElement = document.createElement(NODE);
                        nodeElement.setAttribute(DURATION, Integer.toString(musicalElement.getDifference()));
                        sequenceElement.appendChild(nodeElement);
                        if(musicalElement instanceof Beat)
                        {
                                Beat beat = (Beat) musicalElement;
                                nodeElement.setAttribute(MEASURE, Boolean.toString(beat.isMeasureBeat()));
                        }
                        else if(musicalElement instanceof Chord)
                        {
                                Chord chord = (Chord) musicalElement;
                                nodeElement.setAttribute(CHORD, chord.getChord());
                        }
                        else if(musicalElement instanceof Clef)
                        {
                                Clef clef = (Clef) musicalElement;
                                nodeElement.setAttribute(TYPE, Clef.Type.toString(clef.getType()));
                                nodeElement.setAttribute(STAFFRANK, Integer.toString(clef.getStaffRank()));
                                nodeElement.setAttribute(CENTRALCRANK, Integer.toString(clef.getCentralCRank()));
                        }
                        else if(musicalElement instanceof KeySignature)
                        {
                                KeySignature keySignature = (KeySignature) musicalElement;
                                nodeElement.setAttribute(TYPE, Integer.toString(keySignature.getType()));
                        }
                        else if(musicalElement instanceof Lyric)
                        {
                                Lyric lyric = (Lyric) musicalElement;
                                nodeElement.setAttribute(LYRIC, lyric.getLyric());
                        }
                        else if(musicalElement instanceof TimeSignature)
                        {
                                TimeSignature timeSignature = (TimeSignature) musicalElement;
                                nodeElement.setAttribute(NOMINATOR, Integer.toString(timeSignature.getNominator()));
                                nodeElement.setAttribute(DENOMINATOR, Integer.toString(timeSignature.getDenominator()));
                        }
                        else if(musicalElement instanceof Tone)
                        {
                                Tone tone = (Tone) musicalElement;
                                if(tone.getBase40Rank() != null)
                                        nodeElement.setAttribute(BASE40RANK, Integer.toString(tone.getBase40Rank()));
                        }
                }
                return sequenceElement;
        }

        private Sequence createSequence(Element sequenceElement)
        {
                String type = sequenceElement.getAttribute(TYPE);
                if(type.compareTo(Beat.class.getSimpleName()) == 0)
                {
                        return createBeatSequence(sequenceElement);
                }
                else if(type.compareTo(Chord.class.getSimpleName()) == 0)
                {
                        return createChordSequence(sequenceElement);
                }
                else if(type.compareTo(Clef.class.getSimpleName()) == 0)
                {
                        return createClefSequence(sequenceElement);
                }
                else if(type.compareTo(KeySignature.class.getSimpleName()) == 0)
                {
                        return createKeySignatureSequence(sequenceElement);
                }
                else if(type.compareTo(Lyric.class.getSimpleName()) == 0)
                {
                        return createLyricSequence(sequenceElement);
                }
                else if(type.compareTo(TimeSignature.class.getSimpleName()) == 0)
                {
                        return createTimeSignatureSequence(sequenceElement);
                }
                else if(type.compareTo(Tone.class.getSimpleName()) == 0)
                {
                        return createToneSequence(sequenceElement);
                }
                return null;
        }

        private Sequence<Beat> createBeatSequence(Element beatSequenceElement)
        {
                Sequence<Beat> beatSequence = new Sequence<Beat>(Beat.class);
                NodeList nodes = beatSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        boolean measure = Boolean.parseBoolean(nodeElement.getAttribute(MEASURE));
                        beatSequence.add(new Beat(duration).setMeasureBeat(measure));
                }
                return beatSequence;
        }

        private Sequence<Chord> createChordSequence(Element chordSequenceElement)
        {
                Sequence<Chord> chordSequence = new Sequence<Chord>(Chord.class);
                NodeList nodes = chordSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        String chord = nodeElement.getAttribute(CHORD);
                        chordSequence.add(new Chord(duration).setChord(chord));
                }
                return chordSequence;
        }

        private Sequence<Clef> createClefSequence(Element clefSequenceElement)
        {
                Sequence<Clef> clefSequence = new Sequence<Clef>(Clef.class);
                NodeList nodes = clefSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        Clef.Type type = Clef.Type.toType(nodeElement.getAttribute(TYPE));
                        int staffRank = Integer.parseInt(nodeElement.getAttribute(STAFFRANK));
                        int centralCRank = Integer.parseInt(nodeElement.getAttribute(CENTRALCRANK));
                        clefSequence.add(new Clef(duration).setType(type).setStaffRank(staffRank)
                                                           .setCentralCRank(centralCRank));
                }
                return clefSequence;
        }

        private Sequence<KeySignature> createKeySignatureSequence(Element keySignatureSequenceElement)
        {
                Sequence<KeySignature> keySignatureSequence = new Sequence<KeySignature>(KeySignature.class);
                NodeList nodes = keySignatureSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        int type = Integer.parseInt(nodeElement.getAttribute(TYPE));
                        keySignatureSequence.add(new KeySignature(duration).setType(type));
                }
                return keySignatureSequence;
        }

        private Sequence<Lyric> createLyricSequence(Element lyricSequenceElement)
        {
                Sequence<Lyric> lyricSequence = new Sequence<Lyric>(Lyric.class);
                NodeList nodes = lyricSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        String lyric = nodeElement.getAttribute(LYRIC);
                        lyricSequence.add(new Lyric(duration).setLyric(lyric));
                }
                return lyricSequence;
        }

        private Sequence<TimeSignature> createTimeSignatureSequence(Element timeSignatureSequenceElement)
        {
                Sequence<TimeSignature> timeSignatureSequence = new Sequence<TimeSignature>(TimeSignature.class);
                NodeList nodes = timeSignatureSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        int nominator = Integer.parseInt(nodeElement.getAttribute(NOMINATOR));
                        int denominator = Integer.parseInt(nodeElement.getAttribute(DENOMINATOR));
                        timeSignatureSequence.add(new TimeSignature(duration).setNominator(nominator)
                                                                             .setDenominator(denominator));
                }
                return timeSignatureSequence;
        }

        private Sequence<Tone> createToneSequence(Element toneSequenceElement)
        {
                ToneSequence toneSequence = new ToneSequence();
                NodeList nodes = toneSequenceElement.getElementsByTagName(NODE);
                for(int i = 0; i < nodes.getLength(); i++)
                {
                        Element nodeElement = (Element) nodes.item(i);
                        int duration = Integer.parseInt(nodeElement.getAttribute(DURATION));
                        String base40Rank = nodeElement.getAttribute(BASE40RANK);
                        if(base40Rank != null && !base40Rank.isEmpty())
                                toneSequence.add(new Tone(duration).setBase40Rank(
                                        Integer.parseInt(nodeElement.getAttribute(BASE40RANK))));
                        else
                                toneSequence.add(new Tone(duration));
                }
                return toneSequence;
        }
}
